#ifndef ARRAY_HH
#define ARRAY_HH

#include <algorithm>

//////////////////////////////////
//	constructors
//////////////////////////////////

template <typename T, unsigned int DIM>
array<T, DIM>::array( const array<T, DIM>& ref_arr )
{
	*this = ref_arr;
}

template <typename T, unsigned int DIM >
array<T, DIM>& array<T, DIM>::operator=( const array<T, DIM>& ref_arr )
{
	int num_elem = (*ref_arr.data_p_).size();
	data_p_ = shared_ptr<storage_type>( new storage_type( num_elem ) );
	std::copy( (*ref_arr.data_p_).begin(), (*ref_arr.data_p_).end(), (*data_p_).begin() );
	
	storage_ = ref_arr.storage_;
	shape_ = ref_arr.shape_;
	stride_ = ref_arr.stride_;
	p_zero_offset_ = ref_arr.p_zero_offset_;
	return *this;
}

template <typename T, unsigned int DIM>
array<T, DIM>::array( array<T, DIM>&& arr )
{
	*this = std::move( arr );
}
	
template <typename T, unsigned int DIM>
array<T, DIM>& array<T, DIM>::operator=( array<T, DIM>&& ref_arr )
{
	data_p_ = std::move( ref_arr.data_p_ );
	storage_ = std::move( ref_arr.storage_ );
	shape_ = std::move( ref_arr.shape_ );
	stride_ = std::move( ref_arr.stride_ );
	p_zero_offset_ = std::move( ref_arr.p_zero_offset_ );
	return *this;
}

template <typename T, unsigned int DIM>
array<T, DIM>::array( 
	std::initializer_list<int> shape,
	const array_storage<DIM>& storage )
	: storage_(storage), stride_(1)
{
	ASSERT( shape.size() == DIM, "length of dimensions does not match" );
	shape_ = tiny_vector<int, DIM>( shape );

	create_new_array();
	array_helper::init_pzero_offset( *this, p_zero_offset_ );
}

template <typename T, unsigned int DIM>
array<T, DIM>::array( 
	const tiny_vector<int, DIM>& shape, 
	const array_storage<DIM>& storage)
	: storage_(storage), shape_(shape), stride_(1)
{
	ASSERT( shape.size() == DIM, "length of dimensions does not match" );
	
	create_new_array();
	array_helper::init_pzero_offset( *this, p_zero_offset_ );
}

template <typename T, unsigned int DIM>
array<T, DIM>::array(
	const tiny_vector<range, DIM>& range,
	const array_storage<DIM>& storage )
	: storage_(storage), stride_(1)
{
	tiny_vector<int, DIM> base;
	for( unsigned int dim = 0; dim < DIM; dim++ )
	{
		base[dim] = range[dim].start();
		shape_[dim] = range[dim].length();
	}
	storage_.set_base( base );
	
	create_new_array();	
	array_helper::init_pzero_offset( *this, p_zero_offset_ );
}

//! for slicing
template <typename T, unsigned int DIM>
array<T, DIM>::array( array& ref_arr, const tiny_vector<range, DIM>& new_slice )
{
	reference( ref_arr );
	for( unsigned int dim = 0; dim < DIM; dim++ )
		slice( dim, new_slice[dim] );
}
		
//////////////////////////////////
//	methods
//////////////////////////////////

template <typename T, unsigned int DIM>
void array<T, DIM>::create_new_array()
{
	int total_size = 1;
	for( auto v : shape_ )	total_size *= v;
	data_p_ = shared_ptr<storage_type>( new storage_type( total_size ) );
	stride_ = storage_.compute_storage_stride( shape_ );
	p_zero_offset_ = 0;
}

template <typename T, unsigned int DIM>
void array<T, DIM>::reference( array<T, DIM>& ref_arr )
{
	data_p_ = ref_arr.data_p_;
	storage_ = ref_arr.storage_;
	shape_ = ref_arr.shape_;
	stride_ = ref_arr.stride_;
	p_zero_offset_ = ref_arr.p_zero_offset_;
}

template <typename T, unsigned int DIM>
void array<T, DIM>::slice( unsigned int dim, range r )
{
	auto new_first  = r.start();
	auto new_stride = r.stride();
	auto new_length = r.length();
	
	p_zero_offset_ += ( new_first - lbound(dim) * new_stride) * stride_[dim];
	shape_[dim] = new_length;
	stride_[dim] *= new_stride;	
}

template <typename T, unsigned int DIM>
const T* array<T, DIM>::data_first() const
{
	int p = p_zero_offset_;
	auto id0 = lbound();
	for( unsigned int dim = 0; dim < DIM; dim++ )
		p += id0[dim] * stride_[dim];
	return &(*data_p_.get())[p];
}

template <typename T, unsigned int DIM>
T* array<T, DIM>::data_first()
{
	int p = p_zero_offset_;
	auto id0 = lbound();
	for( unsigned int dim = 0; dim < DIM; dim++ )
		p += id0[dim] * stride_[dim];
	return &(*data_p_.get())[p];
}

template <typename T, unsigned int DIM>
array<T, DIM>& array<T, DIM>::operator=( std::initializer_list<T> vals)
{
	auto iter1 = begin();
	auto iter2 = vals.begin();
	for( ; iter1 != end() && iter2 != vals.end(); ++iter1, ++iter2 )
		*iter1 = *iter2;
}

///////////////////////////////////////////////
//	subscripting / slicing / resize
///////////////////////////////////////////////

template <typename T, unsigned int DIM>
template <typename... ID_types>
typename array_helper::array_slice<array<T, DIM>, ID_types...>::return_type
array<T, DIM>::operator()( ID_types... id )
{
	return array_helper::array_slice<array_type, ID_types...>::get( *this, id... );
}

template <typename T, unsigned int DIM>
template <typename... ID_types>
typename array_helper::array_slice<const array<T, DIM>, ID_types...>::return_type
array<T, DIM>::operator()( ID_types... id ) const
{
	return array_helper::array_slice<const array_type, ID_types...>::get( *this, id... );
}

template <typename T, unsigned int DIM>
template <typename... ID_types>
void array<T, DIM>::resize( ID_types&&... args )
{
	array_helper::array_resize<array_type, ID_types...>::resize( *this, std::forward<ID_types>(args)... );
	create_new_array();
	std::cout << "new lbound = " << lbound() << std::endl;
	std::cout << "new ubound = " << ubound() << std::endl;
}

//////////////////////////////////////////////////////
//	operator overloading -- binary assign
//	-- On const value
//////////////////////////////////////////////////////

#define DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( name, op )		\
template <typename T, unsigned int DIM>			\
array<T, DIM>& array<T, DIM>::operator op( const T const_val )	\
{									\
	using array_binary_ops::name;					\
	iterator_on_one_element<T> iter_wrap( const_val );		\
	array_binary_apply<name>::go( begin(), end(), iter_wrap );	\
	return *this;							\
}

DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( equal, = );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( plus_equal, += );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( minus_equal, -= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( multiply_equal, *= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( divide_equal, /= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( mod_equal, %= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( xor_equal, ^= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( bitand_equal, &= );
DECLARE_ARRAY_BIN_OPS_ON_CONST_VAL( bitor_equal, |= );

template <typename T, unsigned int DIM>
array<T, DIM>& array<T, DIM>::operator>>=( const int shift )
{				
	using array_binary_ops::right_shift_equal;
	iterator_on_one_element<int> iter_wrap( shift );		
	array_binary_apply<right_shift_equal>::go( begin(), end(), iter_wrap );		
	return *this;
}

template <typename T, unsigned int DIM>
array<T, DIM>& array<T, DIM>::operator<<=( const int shift )
{				
	using array_binary_ops::left_shift_equal;
	iterator_on_one_element<int> iter_wrap( shift );		
	array_binary_apply<left_shift_equal>::go( begin(), end(), iter_wrap );		
	return *this;
}

//////////////////////////////////////////////////////
//	operator overloading -- binary assign
//	-- on a array
//////////////////////////////////////////////////////

#define DECLARE_ARRAY_BIN_OPS_ON_ARRAY( name, op )			\
template <typename T, unsigned int DIM >				\
array<T, DIM>& array<T, DIM>::operator op( const array<T, DIM>& vec ) 	\
{									\
	using array_binary_ops::name;					\
	array_binary_apply<name>::go( begin(), end(), vec.begin() );	\
	return *this;							\
}

DECLARE_ARRAY_BIN_OPS_ON_ARRAY( plus_equal, += );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( minus_equal, -= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( multiply_equal, *= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( divide_equal, /= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( mod_equal, %= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( xor_equal, ^= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( bitand_equal, &= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( bitor_equal, |= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( right_shift_equal, >>= );
DECLARE_ARRAY_BIN_OPS_ON_ARRAY( left_shift_equal, <<= );
	
//////////////////////////////////////////////////////
//	operator overloading -- binary logical
//////////////////////////////////////////////////////

#define DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( name, op )	\
template <typename T, unsigned int DIM >			\
bool array<T, DIM>::operator op( const array<T, DIM>& vec )	\
{								\
	using array_binary_logical_ops::name;			\
	return	array_binary_logical_apply<name>		\
		::go( begin(), end(), vec.begin() );		\
}

DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( equal, == );
DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( not_equal, != );
/*
DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( greater, > );
DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( less, < );
DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( greater_equal, >= );
DECLARE_ARRAY_BIN_LOGIC_OPS_ON_ARRAY( less_equal, <= );
*/

#endif //!< ARRAY_HH